/********************************************************************************
 * Copyright (c) 2011-2017 Red Hat Inc. and/or its affiliates and others
 *
 * This program and the accompanying materials are made available under the 
 * terms of the Apache License, Version 2.0 which is available at
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * SPDX-License-Identifier: Apache-2.0 
 ********************************************************************************/
package org.eclipse.ceylon.compiler.typechecker;

import java.util.List;

import org.eclipse.ceylon.cmr.api.RepositoryManager;
import org.eclipse.ceylon.common.Versions;
import org.eclipse.ceylon.compiler.typechecker.analyzer.ModuleValidator;
import org.eclipse.ceylon.compiler.typechecker.context.Context;
import org.eclipse.ceylon.compiler.typechecker.context.PhasedUnit;
import org.eclipse.ceylon.compiler.typechecker.context.PhasedUnits;
import org.eclipse.ceylon.compiler.typechecker.io.VFS;
import org.eclipse.ceylon.compiler.typechecker.io.VirtualFile;
import org.eclipse.ceylon.compiler.typechecker.tree.Message;
import org.eclipse.ceylon.compiler.typechecker.util.AssertionVisitor;
import org.eclipse.ceylon.compiler.typechecker.util.ModuleManagerFactory;
import org.eclipse.ceylon.compiler.typechecker.util.StatisticsVisitor;
import org.eclipse.ceylon.model.typechecker.model.Module;

/**
 * Executes type checking upon construction and retrieve a 
 * CompilationUnit object for a given File.
 *
 * @author Emmanuel Bernard <emmanuel@hibernate.org>
 */
public class TypeChecker {

    public static final String LANGUAGE_MODULE_VERSION = 
            Versions.CEYLON_VERSION_NUMBER;

    private final boolean verbose;
    private final boolean statistics;
    private final Context context;
    private final PhasedUnits phasedUnits;
    private List<PhasedUnits> phasedUnitsOfDependencies;
    private final boolean verifyDependencies;
    private final AssertionVisitor assertionVisitor;
    private final StatisticsVisitor statsVisitor;

    TypeChecker(VFS vfs, 
            List<VirtualFile> srcDirectories, 
            RepositoryManager repositoryManager, 
            boolean verifyDependencies,
            AssertionVisitor assertionVisitor, 
            ModuleManagerFactory moduleManagerFactory, 
            boolean verbose, boolean statistics,
            List<String> moduleFilters, 
            List<VirtualFile> srcFiles, 
            String encoding) {
        long start = System.nanoTime();
        this.verbose = verbose;
        this.statistics = statistics;
        this.context = new Context(repositoryManager, vfs);
        this.phasedUnits = 
                new PhasedUnits(context, 
                        moduleManagerFactory);
        this.verifyDependencies = verifyDependencies;
        this.assertionVisitor = assertionVisitor;
        statsVisitor = new StatisticsVisitor();
        phasedUnits.setSourceFiles(srcFiles);
        phasedUnits.setModuleFilters(moduleFilters);
        phasedUnits.setEncoding(encoding);
        phasedUnits.parseUnits(srcDirectories);
        long time = System.nanoTime()-start;
        if(statistics)
            System.out.println("Parsed in " + 
                    time/1000000 + " ms");
    }

    public PhasedUnits getPhasedUnits() {
        return phasedUnits;
    }
    
    public List<PhasedUnits> getPhasedUnitsOfDependencies() {
        return phasedUnitsOfDependencies;
    }
    
    public void setPhasedUnitsOfDependencies(
            List<PhasedUnits> phasedUnitsOfDependencies) {
        this.phasedUnitsOfDependencies = 
                phasedUnitsOfDependencies;
    }

    public Context getContext() {
        return context;
    }

    /**
     * Return the PhasedUnit for a given relative path.
     * The path is relative to the source directory
     * eg ceylon/language/Object.ceylon
     */
    public PhasedUnit getPhasedUnitFromRelativePath(
            String relativePath) {
        PhasedUnit phasedUnit = 
                phasedUnits.getPhasedUnitFromRelativePath(
                        relativePath);
        if (phasedUnit == null) {
            for (PhasedUnits units: 
                    phasedUnitsOfDependencies) {
                phasedUnit = 
                        units.getPhasedUnitFromRelativePath(
                                relativePath);
                if (phasedUnit != null) {
                    return phasedUnit;
                }
            }
            return null;
        }
        else {
            return phasedUnit;
        }
    }

    public PhasedUnit getPhasedUnit(VirtualFile file) {
        PhasedUnit phasedUnit = 
                phasedUnits.getPhasedUnit(file);
        if (phasedUnit == null) {
            for (PhasedUnits units: 
                    phasedUnitsOfDependencies) {
                phasedUnit = units.getPhasedUnit(file);
                if (phasedUnit != null) {
                    return phasedUnit;
                }
            }
            return null;
        }
        else {
            return phasedUnit;
        }
    }
    
    public void process() 
            throws RuntimeException {
        process(false);
    }
    
    public void process(boolean forceSilence) 
            throws RuntimeException {
        long start = System.nanoTime();
        executePhases(phasedUnits, forceSilence);
        long time = System.nanoTime()-start;
        if(statistics)
            System.out.println("Type checked in " + 
                    time/1000000 + " ms");
    }

    private void executePhases(PhasedUnits phasedUnits, 
            boolean forceSilence) {
        List<PhasedUnit> listOfUnits = 
                phasedUnits.getPhasedUnits();

        phasedUnits.getModuleManager()
            .prepareForTypeChecking();
        phasedUnits.visitModules();
        phasedUnits.getModuleManager()
            .modulesVisited();

        //By now le language module version should be known 
        //(as local) or we should use the default one.
        Module languageModule = 
                context.getModules()
                    .getLanguageModule();
        if (languageModule.getVersion() == null) {
            languageModule.setVersion(LANGUAGE_MODULE_VERSION);
        }

        ModuleValidator moduleValidator = 
                new ModuleValidator(context, phasedUnits);
        if (verifyDependencies) {
            moduleValidator.verifyModuleDependencyTree();
        }
        phasedUnitsOfDependencies = 
                moduleValidator.getPhasedUnitsOfDependencies();

        executePhases(listOfUnits);

        if (!forceSilence) {
            for (PhasedUnit pu: listOfUnits) {
                if (verbose) {
                    pu.display();
                }
                pu.generateStatistics(statsVisitor);
                pu.runAssertions(assertionVisitor);
            }
            if (verbose||statistics) {
                statsVisitor.print();
            }
            assertionVisitor.print(verbose);
        }
        
    }

    protected void executePhases(
            List<PhasedUnit> listOfUnits) {
        for (PhasedUnit pu : listOfUnits) {
            pu.validateTree();
            pu.scanDeclarations();
        }
        for (PhasedUnit pu : listOfUnits) {
            pu.scanTypeDeclarations();
        }
        for (PhasedUnit pu: listOfUnits) {
            pu.validateRefinement();
        }
        for (PhasedUnit pu : listOfUnits) {
            pu.analyseTypes();
        }
        for (PhasedUnit pu: listOfUnits) {
            pu.analyseFlow();
        }
        for (PhasedUnit pu: listOfUnits) {
            pu.analyseUsage();
        }
    }
    
    public int getErrors(){
        return assertionVisitor.getErrors();
    }

    public int getWarnings(){
        return assertionVisitor.getWarnings();
    }
    
    public List<Message> getMessages(){
        return assertionVisitor.getFoundErrors();
    }
}
